﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.
using System;
using System.Cloud.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Text;
using System.Threading;
using System.Threading.Actors;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;

namespace ActorFramework.Fakebook
{
    /// <summary>
    /// Interaction logic for UserHomePage.xaml
    /// </summary>
    public partial class UserHomePage : Window
    {
        private String _viewingAccountName;
        private FakebookPerson _clientForThisUser;
        private bool _viewingMyOwnPage;
        private ObservableCloudList<String> _friends;
        private IDisposable _newFriendSubscription;
        private IDisposable _friendRequestCompletedSubscription;
        private IDisposable _profileChangeSubscription;
        private NewsFeed _newsfeed;
        private IDisposable _newsfeedSubscription;
        private SynchronizationContext _uiThreadSyncContext;
        private CloudList<String> _myNewsPosts;
        private CloudStringDictionary<PictureInfo> _photos;

        // For refreshing after someone accepts friend requests
        internal static Dictionary<String, UserHomePage> _windowsForUser = new Dictionary<string, UserHomePage>();

        public UserHomePage(String viewingAccountName)
        {
            InitializeComponent();
            Interlocked.Increment(ref App.NumInterestingWindowsOpen);
            this.Title = "Fakebook: Logged in as " + App.FakebookClient.FullName;

            _viewingAccountName = viewingAccountName;
            _uiThreadSyncContext = SynchronizationContext.Current;

            _viewingMyOwnPage = viewingAccountName == App.AccountName;
            if (_viewingMyOwnPage)
            {
                // I'm looking at my own page
                _clientForThisUser = App.FakebookClient;

                // Reuse the "add friend" button for editing the current user's profile
                FriendButton.Content = "Edit Profile";

                // Subscribe to new friend requests
                _newFriendSubscription = _clientForThisUser.Subscribe("NewFriendRequest", new NewFriendRequestObserver(this, Dispatcher));

                // Subscribe to processing the result of friend requests
                _friendRequestCompletedSubscription = _clientForThisUser.Subscribe("FriendRequestProcessed", new FriendRequestCompletedObserver(this, Dispatcher));

                // Get the newsfeed aggregator actor and subscribe to it.
                _newsfeed = new NewsFeed(App.FabricAddress, App.AccountName, App.ConnectThroughGateway);
                _newsfeedSubscription = _newsfeed.Subscribe(new NewsFeedObserver(this, Dispatcher));
            }
            else
            {
                _clientForThisUser = new FakebookPerson(App.FabricAddress, viewingAccountName, App.ConnectThroughGateway);

                if (_friends == null)
                    _friends = _clientForThisUser.GetFriendsList(_uiThreadSyncContext);                
                bool isMyFriend = _friends.Contains(App.AccountName);
                if (isMyFriend)
                {
                    FriendButton.Content = "Your Friend";
                    FriendButton.IsEnabled = false;
                }

                NewsfeedLabel.Opacity = 0;
                NewsPostTextBox.Opacity = 0;
                NewsPostTextBox.IsEnabled = false;
                NewsPostButton.Opacity = 0;
                NewsPostButton.IsEnabled = false;

                // Make sure you can't remove someone else's friends!
                UnfriendContextMenuItem.IsEnabled = false;
            }

            _profileChangeSubscription = _clientForThisUser.Subscribe("ProfileChange", Observer.Create<ActorEvent>(actorEvent => GetProfileInfo()));

            lock (_windowsForUser)
                _windowsForUser[viewingAccountName] = this;

            String firstName = _clientForThisUser.FirstName;
            String lastName = _clientForThisUser.LastName;
            PersonNameTextBlock.Text = firstName + " " + lastName;

            Refresh();
        }

        private void Refresh()
        {
            Task.Run(() => GetProfileInfo());
            Task.Run(() => GetFriends());
            Task.Run(() => GetPhotos());

            if (!_viewingMyOwnPage)
            {
                bool isMyFriend;
                var theirFriends = _clientForThisUser.GetFriendsList(_uiThreadSyncContext);
                    isMyFriend = theirFriends.Contains(App.AccountName);

                if (isMyFriend)
                {
                    Dispatcher.Invoke(() =>
                    {
                        FriendButton.Content = "Your Friend";
                        FriendButton.IsEnabled = false;
                    });
                }
            }
        }

        // Don't call this on the UI thread.
        private void GetProfileInfo()
        {
            StringBuilder profileInfo = new StringBuilder();
            String city = _clientForThisUser.City;
            if (city != null)
                profileInfo.AppendFormat("Lives in {0}\r\n", city);
            RelationshipStatus relStatus = _clientForThisUser.RelationshipStatus;
            switch (relStatus)
            {
                case RelationshipStatus.Unspecified:
                    break;

                case RelationshipStatus.Single:
                case RelationshipStatus.Married:
                case RelationshipStatus.Engaged:
                case RelationshipStatus.Divorced:
                    profileInfo.AppendFormat("Is {0}\r\n", relStatus);
                    break;

                case RelationshipStatus.Other:
                    profileInfo.AppendFormat("In a non-traditional relationship\r\n");
                    break;

                default:
                    Contract.Assert(false, "Unrecognized relationship status");
                    break;
            }

            DateTimeOffset? birthday = _clientForThisUser.Birthday;
            if (birthday.HasValue)
            {
                profileInfo.AppendFormat("Born on {0}\r\n", birthday.Value.Date.ToString("MMMM d"));
            }

            PictureInfo profilePicture = _clientForThisUser.ProfilePicture;
            byte[] profileImage = profilePicture.ImageBytes;

            String profileString = profileInfo.ToString();
            Dispatcher.InvokeAsync(() =>
            {
                ProfileInfoTextBlock.Text = profileString;
                if (profileImage != null) {
                    BitmapImage bi = new BitmapImage();
                    bi.BeginInit();
                    bi.StreamSource = new MemoryStream(profileImage);
                    bi.EndInit();

                    Image image = new Image();
                    image.Source = bi;
                    ProfilePicture.Content = image;
                    ProfilePicture.ToolTip = MakeToolTip(profilePicture);
                }
            });
        }

        private Object MakeToolTip(PictureInfo picture)
        {
            String toolTipFormat = "{0} by {1}";
            if (picture.LicenseName != null)
                toolTipFormat = "Photo by {1}, {2}";
            return String.Format(toolTipFormat, picture.Title, picture.Author, picture.LicenseName);
        }

        // For clicking on URI's in TextBlocks
        private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
        {
            System.Diagnostics.Process.Start(e.Uri.AbsoluteUri);
        }

        bool friendsObservableTransformListInitialized = false;

        // Don't call this on the UI thread.
        private void GetFriends()
        {
            if (_friends == null)
                _friends = _clientForThisUser.GetFriendsList(_uiThreadSyncContext);

            if (friendsObservableTransformListInitialized == false)
            {
                // Here, we need an ObservableCloudList<String> converted to an observable sequence
                // that also passes through INotifyCollectionChanged events.  When exploring how to 
                // build this, we settled on using a Select LINQ operator for our type.  Due to 
                // some problems with C# method binding rules, we had to add an interface
                // to express the right type information.  But, this works.
                var people = _friends.Select(accountName =>
                    new FakebookPerson(App.FabricAddress, accountName, App.ConnectThroughGateway));
                // Note - LINQ's Where loses the INotifyCollectionChanged aspect.  Look at the OLinq Nuget package.
                //var people2 = people.Where(person => true);
                Dispatcher.Invoke(() => FriendsListBox.ItemsSource = people);
                friendsObservableTransformListInitialized = true;
            }
        }

        private void GetPhotos()
        {
            if (_photos == null)
                _photos = _clientForThisUser.Photos;

            // Read all pictures while we're not on the UI thread.
            // @TODO: Consider making the values collection of a CloudStringDictionary implement 
            // INotifyCollectionChanged so we could use Select & databinding here.
            var allPhotos = _photos.Values.ToArray();

            Dispatcher.Invoke(() =>
            {
                PhotosStackPanel.Children.Clear();
                foreach (var picture in allPhotos)
                {
                    BitmapImage bi = new BitmapImage();
                    bi.BeginInit();
                    bi.StreamSource = new MemoryStream(picture.ImageBytes);
                    bi.EndInit();
                    Image image = new Image();
                    image.Source = bi;
                    image.Height = 68;

                    // We could add the image with a tooltip directly to the photos stack panel, but
                    // we want people to be able to click on the images.  That requires adding a button instead.
                    Button button = new Button();
                    button.Content = image;
                    button.Tag = picture;  // To display the right picture later.
                    button.ToolTip = MakeToolTip(picture);
                    button.Click += DisplayPhotoPage;

                    PhotosStackPanel.Children.Add(button);

                    // Space out the images.  No obvious way to do this in a data template.
                    // I'll add a useless control instead.
                    TextBlock padding = new TextBlock();
                    padding.Text = "    ";
                    PhotosStackPanel.Children.Add(padding);
                }
            });
        }

        private void DisplayPhotoPage(object sender, RoutedEventArgs e)
        {
            Button button = (Button)sender;
            PictureInfo picture = (PictureInfo)button.Tag;
            Window window = new PictureWindow(picture);
            window.Show();
        }

        private void FriendButton_Click(object sender, RoutedEventArgs e)
        {
            if (_viewingMyOwnPage)
            {
                var newUser = new EditProfile(false);
                newUser.Show();
                //this.Close();
            }
            else
            {
                // Make a new friend request, etc.
                _clientForThisUser.MakeFriendRequest(App.AccountName);
            }
        }

        protected override void OnClosed(EventArgs e)
        {
            try
            {
                lock (_windowsForUser)
                    _windowsForUser.Remove(_viewingAccountName);

                if (_myNewsPosts != null)
                {
                    _myNewsPosts.Dispose();
                    _myNewsPosts = null;
                }

                if (_newFriendSubscription != null)
                {
                    _newFriendSubscription.Dispose();
                    _newFriendSubscription = null;
                }
                if (_friendRequestCompletedSubscription != null)
                {
                    _friendRequestCompletedSubscription.Dispose();
                    _friendRequestCompletedSubscription = null;
                }
                if (_newsfeedSubscription != null)
                {
                    _newsfeedSubscription.Dispose();
                    _newsfeedSubscription = null;
                }
                if (_profileChangeSubscription != null)
                {
                    _profileChangeSubscription.Dispose();
                    _profileChangeSubscription = null;
                }

                if (_friends != null)
                    _friends.Dispose();
                if (_newsfeed != null)
                    _newsfeed.Dispose();
                if (_photos != null)
                    _photos.Dispose();
            }
            finally
            {
                int numInterestingWindowsOpen = Interlocked.Decrement(ref App.NumInterestingWindowsOpen);

                base.OnClosed(e);

                if (numInterestingWindowsOpen == 0)
                    Environment.Exit(0);
            }
        }

        private void SearchButton_Click(object sender, RoutedEventArgs e)
        {
            String searchString = SearchTextBox.Text;

            Task.Run(() =>
            {
                CloudList<String> allUsers = new CloudList<string>(App.FabricAddress, "AllFakebookUsers", App.ConnectThroughGateway);

                String targetAccountName = null;
                if (allUsers.Contains(searchString))
                    targetAccountName = searchString;
                else
                {
                    var matches = allUsers.Where(accountName =>
                    {
                        FakebookPerson user = new FakebookPerson(App.FabricAddress, accountName, App.ConnectThroughGateway);
                        bool ret = user.FullName == searchString || user.FirstName == searchString || user.LastName == searchString;
                        user.Dispose();
                        return ret;
                    });
                    if (matches.Count() > 0)
                        targetAccountName = matches.First();
                }

                if (targetAccountName != null)
                {
                    Dispatcher.Invoke(() => new UserHomePage(targetAccountName).Show());
                }
            });
        }

        #region Friend Context Menu Handlers

        private void ViewFriend_Click(object sender, RoutedEventArgs e)
        {
            Object obj = FriendsListBox.SelectedItem;
            FakebookPerson friendToView = (FakebookPerson)obj;
            new UserHomePage(friendToView.AccountName).Show();
        }

        private void SendPrivateMessage_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("SendPrivateMessage NYI");
        }

        private void Unfriend_Click(Object sender, RoutedEventArgs e)
        {
            Object obj = FriendsListBox.SelectedItem;
            FakebookPerson friendToView = (FakebookPerson)obj;
            _clientForThisUser.Unfriend(friendToView.AccountName);
            Task.Run(() => GetFriends());
        }

        #endregion Friend Context Menu Handlers

        #region Observers

        private class NewFriendRequestObserver : IObserver<ActorEvent>
        {
            UserHomePage _window;
            Dispatcher _dispatcher;

            internal NewFriendRequestObserver(UserHomePage window, Dispatcher dispatcher)
            {
                _window = window;
                _dispatcher = dispatcher;
            }

            void IObserver<ActorEvent>.OnCompleted()
            {                
            }

            void IObserver<ActorEvent>.OnError(Exception error)
            {
                throw new NotImplementedException("NewFriendObserver.OnError NYI.  Real problem: " + error);
            }

            void IObserver<ActorEvent>.OnNext(ActorEvent value)
            {
                String potentialFriend = value.GetTypedPayload<String>();
                FakebookPerson potentialFriendClient = new FakebookPerson(App.FabricAddress, potentialFriend, App.ConnectThroughGateway);
                String friendFullName = potentialFriendClient.FullName;
                //byte[] profilePicture = potentialFriendClient.ProfilePicture;
                potentialFriendClient.Dispose();
                String myName = App.FakebookClient.FirstName;

                _dispatcher.InvokeAsync(() =>
                {
                    MessageBoxResult res = MessageBox.Show(String.Format("Would you like to be {0}'s friend?", friendFullName), 
                        String.Format("New Friend Request for {0}", myName), MessageBoxButton.YesNo, MessageBoxImage.Question);
                    if (res == MessageBoxResult.Yes)
                    {
                        Task.Run(() =>
                        {
                            App.FakebookClient.AcceptFriendRequest(potentialFriend);
                            _window.GetFriends();
                        });
                    }
                    else
                    {
                        Task.Run(() => App.FakebookClient.RejectFriendRequest(potentialFriend));
                    }
                });
            }
        }

        private class FriendRequestCompletedObserver : IObserver<ActorEvent>
        {
            UserHomePage _window;
            Dispatcher _dispatcher;

            internal FriendRequestCompletedObserver(UserHomePage window, Dispatcher dispatcher)
            {
                _window = window;
                _dispatcher = dispatcher;
            }

            void IObserver<ActorEvent>.OnCompleted()
            {
            }

            void IObserver<ActorEvent>.OnError(Exception error)
            {
                throw new NotImplementedException("FriendRequestCompletedObserver.OnError NYI.  Real problem: " + error);
            }

            void IObserver<ActorEvent>.OnNext(ActorEvent value)
            {
                String potentialFriend = value.GetTypedPayload<String>();
                bool wasAccepted = value.GetTypedPayloadQualifier<bool>(0);
                String message = null;
                if (value.EventPayloadQualifiers.Length > 1)
                    message = value.GetTypedPayloadQualifier<String>(1);

                String title = wasAccepted ? String.Format("{0}'s friend request accepted", App.FakebookClient.FirstName) : 
                    String.Format("{0}, someone isn't your friend.", App.FakebookClient.FirstName);
                String text = wasAccepted ? String.Format("{0} accepted your friend request.", potentialFriend) :
                    String.Format("{0} rejected  your friend request.  {1}", potentialFriend, message ?? "");

                Task.Run(() =>
                {
                    _dispatcher.Invoke(() =>
                    {
                        MessageBox.Show(text, title);
                    });

                    _window.GetFriends();
                    UserHomePage friendPage = null;
                    if (UserHomePage._windowsForUser.TryGetValue(potentialFriend, out friendPage))
                    {
                        friendPage.GetFriends();
                        _dispatcher.Invoke(() => friendPage.FriendButton.IsEnabled = false);
                    }
                });                 
            }
        }

        private class NewsFeedObserver : IObserver<ActorEvent>
        {
            UserHomePage _window;
            Dispatcher _dispatcher;

            internal NewsFeedObserver(UserHomePage window, Dispatcher dispatcher)
            {
                _window = window;
                _dispatcher = dispatcher;
            }

            void IObserver<ActorEvent>.OnCompleted()
            {
            }

            void IObserver<ActorEvent>.OnError(Exception error)
            {
                throw new NotImplementedException("NewsfeedObserver.OnError NYI.  Real problem: " + error);
            }

            void IObserver<ActorEvent>.OnNext(ActorEvent value)
            {
                String friendAccountName = value.GetTypedPayload<String>();
                String message = value.GetTypedPayloadQualifier<String>(0);

                bool isSelf = friendAccountName == App.AccountName;

                FakebookPerson friendClient = null;
                String friendFullName = null;
                if (isSelf) {
                    friendClient = App.FakebookClient;
                    Contract.Assume(friendClient != null, "App invariant");
                    friendFullName = "You";
                }
                else
                {
                    friendClient = new FakebookPerson(App.FabricAddress, friendAccountName, App.ConnectThroughGateway);
                    friendFullName = friendClient.FullName;
                }
                PictureInfo friendPicture = friendClient.ProfilePicture;
                byte[] profilePicture = friendPicture.ImageBytes;
                if (!isSelf)
                    friendClient.Dispose();

                // It looks like JSON.NET turns \r\n into "\\r\\n".
                message = message.Replace("\\r\\n", "\r\n");

                NewsFeedUIItem item = new NewsFeedUIItem();
                item.AccountName = friendAccountName;
                item.AuthorFullName = friendFullName;
                item.Message = message;
                item.AuthorPicture = profilePicture;

                _dispatcher.InvokeAsync(() =>
                {
                    _window.NewsFeedListBox.Items.Insert(0, item);
                });                 
            }
        }

        #endregion Observers

        private void NewsPostButton_Click(object sender, RoutedEventArgs e)
        {
            if (_myNewsPosts == null)
            {
                _myNewsPosts = App.FakebookClient.NewsPosts;
            }

            String text = NewsPostTextBox.Text;
            _myNewsPosts.Insert(0, text);
            NewsPostTextBox.Text = String.Empty;
        }

        private void RefreshButton_Click(object sender, RoutedEventArgs e)
        {
            Refresh();
        }

        private void ProfilePicture_Click(object sender, RoutedEventArgs e)
        {
            PictureInfo picture = _clientForThisUser.ProfilePicture;
            Window window = new PictureWindow(picture);
            window.Show();
        }
    }
}
